// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.unit;

import bats.Assets;
import bats.SpatialProperties;
import bats.control.BatsDelayControl;
import bats.control.DeleteControl;
import bats.control.physics.BatsBubbleControl;
import bats.control.physics.GrowControl;
import bats.missile.Shafts;
import bats.scene.SceneNode;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Bone;
import com.jme3.animation.SkeletonControl;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix3f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySkeleton;
import jme3utilities.MySpatial;
import jme3utilities.math.VectorXZ;

/**
 * All the "oto" units in a particular scene.
 * <p>
 * An "oto" is an broad-shouldered golem which can launch pilums. It appears to
 * be encased in black plate armor. The oto model consists of one geometry with
 * 3992 triangles.
 * <p>
 * In addition to the standard unit controls, each simulated oto has Aggressive,
 * Launch, Oto, and OtoAnimation controls.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Otos
        extends UnitClass {
    // *************************************************************************
    // constants

    /**
     * hardness setting (in meters per second)
     */
    final public static float hardness = 10f; // plate armor
    /**
     * when an oto is impaired, it will switch to bubble mode after this time
     * interval (in seconds)
     */
    final private static float impairTimeout = 5f;
    /**
     * maximum ground clearance of a standard-sized oto (in meters)
     */
    final public static float standardMaxClearance = 0.3f;
    /**
     * maximum launch speed of a standard-sized unit (in meters per second)
     */
    final private static float standardMaxLaunchSpeed = 15f;
    /**
     * expected number of geometries in the model
     */
    final private static int geometriesInModel = 1;
    /**
     * message logger for this class
     */
    final private static Logger logger = Logger.getLogger(Otos.class.getName());
    /**
     * asset path to the complete model
     */
    final private static String modelPath = "Models/Oto/Oto.mesh.xml";
    /**
     * pickType for otos
     */
    final private static String pickType = "oto";
    /**
     * model coordinates for the center of an oto's pick indicator
     */
    final private static Vector3f indicatorOffset = new Vector3f(0f, -3f, 0f);
    /**
     * model coordinates of an oto's missile target
     */
    final private static Vector3f targetOffset = new Vector3f(0f, 2.8f, 0f);
    /**
     * model coordinates of the center of an oto's head
     */
    final private static Vector3f headOffset =
            new Vector3f(0f, 4.5f, 0f);
    /**
     * offset of an oto's viewpoint from the center of its head
     */
    final private static Vector3f viewpointOffset =
            new Vector3f(0f, 0f, 0.8f);
    // *************************************************************************
    // constructors

    /**
     * Instantiate for the specified scene.
     *
     * @param scene where these otos reside (not null)
     */
    public Otos(SceneNode scene) {
        super(scene, pickType);

        setStandardMass(120f); // kilograms
        setStandardScale(0.15f);
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create a collision shape for an oto with maximal clearance.
     *
     * @param spatial which oto (not null)
     * @return a new instance
     */
    public CollisionShape createShape(Spatial spatial) {
        float clearance = getMaxClearance(spatial);
        CollisionShape shape = createShape(spatial, clearance);
        return shape;
    }

    /**
     * Create a collision shape which includes a certain portion of the oto's
     * legs plus any missile which the oto is holding.
     *
     * @param spatial which oto (not null)
     * @param clearance ground clearance (in meters, &ge;0)
     * @return a new instance
     */
    public CompoundCollisionShape createShape(Spatial spatial,
            float clearance) {
        assert spatial != null;
        assert clearance >= 0f : clearance;

        float relativeSize = getRelativeSize(spatial);
        CompoundCollisionShape compound = createLeglessShape(spatial);

        float length = 1.03f * relativeSize - clearance; // meters
        float radius = 0.25f * relativeSize; // meters
        float scaleFactor = scene.getScaleFactor();
        CapsuleCollisionShape leg = new CapsuleCollisionShape(
                radius / scaleFactor, length / scaleFactor);
        /*
         * Attach the legs slightly dorsal (to the rear) of center.
         * This causes impaired otos to fall forward.
         */
        float x = 0.10f * relativeSize;
        float y = 0.05f * relativeSize + clearance / 2f;
        float z = -0.03f * relativeSize; // 3cm dorsal of center on standard
        Vector3f offset = new Vector3f(x, y, z);
        offset.divideLocal(scaleFactor);
        compound.addChildShape(leg, offset);

        leg = new CapsuleCollisionShape(radius / scaleFactor,
                length / scaleFactor);
        offset = new Vector3f(-x, y, z);
        offset.divideLocal(scaleFactor);
        compound.addChildShape(leg, offset);
        /*
         * If the oto has loaded a missile, attach that too.
         */
        LaunchControl launchControl = spatial.getControl(LaunchControl.class);
        Spatial missile = launchControl.getMissile();
        if (launchControl.isEnabled() && missile != null) {
            Shafts shafts = scene.getWorld().getShafts();
            CollisionShape shaftShape = shafts.createCollisionShape(missile);
            Vector3f location = missile.getLocalTranslation().clone();
            location.multLocal(spatial.getWorldScale());
            Matrix3f orientation =
                    missile.getLocalRotation().toRotationMatrix();
            compound.addChildShape(shaftShape, location, orientation);
        }
        return compound;
    }

    /**
     * Calculate the maximum ground clearance of a specified oto.
     *
     * @param spatial which oto to measure (not null)
     * @return ground clearance (in meters)
     */
    public float getMaxClearance(Spatial spatial) {
        assert spatial != null;

        float relativeSize = getRelativeSize(spatial);
        float clearance = relativeSize * standardMaxClearance;
        return clearance;
    }

    /**
     * Calculate the maximum launch speed for a specified oto.
     *
     * @param spatial which oto (not null)
     * @return speed (in meters/second)
     */
    public float getMaxLaunchSpeed(Spatial spatial) {
        assert spatial != null;

        float relativeSize = getRelativeSize(spatial);
        float speed = standardMaxLaunchSpeed * relativeSize;
        return speed;
    }
    // *************************************************************************
    // UnitClass methods

    /**
     * Create a unit and add it to the scene and the pick, with its base at the
     * specified world coordinates.
     *
     * @param relativeSize the unit's size relative to the standard (&gt;0)
     * @param unused
     * @param baseLocation world coordinates of the unit's base (not null)
     * @param direction initial direction to face (unit vector)
     * @return a new parented node to represent the unit
     */
    @Override
    public Node add(float relativeSize, String unused, Vector3f baseLocation,
            VectorXZ direction) {
        assert relativeSize > 0f : relativeSize;
        assert baseLocation != null;
        assert direction != null;
        assert direction.isUnitVector() : direction;
        /*
         * Load the "oto" model.
         */
        Node node = Assets.loadModel(modelPath, geometriesInModel);
        /*
         * Set game properties, name, and shadow mode.
         */
        SpatialProperties.setHardness(node, hardness);
        SpatialProperties.setPickType(node, pickType);
        node.setName(uniqueName());
        node.setShadowMode(ShadowMode.Cast);
        /*
         * Detach the geometry from the scene graph because it hasn't
         * been revealed yet.
         */
        Geometry geometry = (Geometry) node.getChild(0);
        node.detachChild(geometry);
        /*
         * Attach the main node to the world.
         */
        scene.getWorld().attach(node);
        /*
         * Set the scale factor.
         */
        float scaleFactor = scene.getScaleFactor();
        float modelScale = standardScale * relativeSize / scaleFactor;
        node.setLocalScale(modelScale);
        assert getRelativeSize(node) == relativeSize : relativeSize;
        /*
         * Initialize the oto's direction.
         */
        float yRotateAngle = FastMath.HALF_PI - direction.azimuth();
        node.rotate(0f, yRotateAngle, 0f);
        /*
         * Initialize the oto's location.
         */
        Vector3f temp = OtoControl.getBase();
        temp.multLocal(-modelScale);
        Vector3f centerInWorld = baseLocation.add(temp);
        MySpatial.setWorldLocation(node, centerInWorld);
        /*
         * Add controls for various behaviors.
         */
        LaunchControl launchControl = new LaunchControl();
        node.addControl(launchControl);

        OtoControl otoControl = new OtoControl(node, scene, direction);
        node.addControl(otoControl);

        SignalControl signalControl = new SignalControl(this);
        node.addControl(signalControl);

        AggressiveControl agressiveControl = new AggressiveControl();
        node.addControl(agressiveControl);

        addAbstractControls(geometry, otoControl, node);

        GrowControl growControl = node.getControl(GrowControl.class);
        if (growControl == null) {
            launchControl.setEnabled(true);
        } else {
            growControl.addSuccessor(launchControl);
            addAnimationControl(node);
        }

        Bone head = MySkeleton.getBone(node, "head");
        head.setUserControl(true);
        resetViewpoint(node);

        assert !MySpatial.isOrphan(node);
        return node;
    }

    /**
     * Get the world coordinates of an oto's pick indicator.
     *
     * @param spatial which oto to locate (not null)
     * @return a new vector
     */
    @Override
    public Vector3f getIndicatorLocation(Spatial spatial) {
        Vector3f result = spatial.localToWorld(indicatorOffset, null);
        return result;
    }

    /**
     * Get the local direction which an oto's pelvis faces.
     *
     * @param spatial which oto to orient (not null)
     * @return a new unit vector
     */
    @Override
    protected Vector3f getModelPelvicDirection(Spatial spatial) {
        assert spatial != null;

        return Vector3f.UNIT_Z.clone();
    }

    /**
     * Get the world locations of an oto's targets.
     *
     * @param spatial which oto to locate (not null)
     * @return a new list of non-null vectors
     */
    @Override
    public Collection<Vector3f> getTargets(Spatial spatial) {
        assert spatial != null;
        /*
         * just one target for now: the oto's core
         */
        ArrayList<Vector3f> result = new ArrayList<>();
        Vector3f core = spatial.localToWorld(targetOffset, null);
        result.add(core);
        return result;
    }

    /**
     * Get the world coordinates of an oto's viewpoint.
     *
     * @param spatial which oto to locate (not null)
     * @return a new vector
     */
    @Override
    public Vector3f getViewpointLocation(Spatial spatial) {
        Vector3f local = getHeadOrientation(spatial).mult(viewpointOffset);
        local.addLocal(headOffset);
        Vector3f world = spatial.localToWorld(local, null);
        return world;
    }

    /**
     * Get the world orientation of an oto's viewpoint.
     *
     * @param spatial which oto to orient (not null)
     * @return a new instance
     */
    @Override
    public Quaternion getViewpointOrientation(Spatial spatial) {
        assert spatial != null;

        Vector3f direction = getWorldPelvicDirection(spatial);
        Quaternion orientation = new Quaternion();
        orientation.lookAt(direction, Vector3f.UNIT_Y);
        Quaternion headOrientation = getHeadOrientation(spatial);
        Quaternion result = orientation.mult(headOrientation);
        return result;
    }

    /**
     * Impair an oto.
     *
     * @param spatial which oto to impair (not null)
     */
    @Override
    public void impair(Spatial spatial) {
        assert spatial != null;

        if (isImpaired(spatial)) {
            /*
             * Once an oto is impaired, this method has no effect.
             */
            return;
        }
        if (SpatialProperties.isHidden(spatial)) {
            /*
             * As long as the oto is hidden, this method has no effect.
             */
            return;
        }
        logger.log(Level.INFO, "impairing {0}", spatial.getName());

        OtoControl otoControl = spatial.getControl(OtoControl.class);
        otoControl.impair();
        /*
         * Un-pick the unit and make it unpickable.
         */
        SpatialProperties.setPicked(spatial, false);
        SpatialProperties.clearPickType(spatial);
        scene.getWorld().attach(spatial);
        /*
         * Release any missile the oto has loaded.
         * TODO other held (but not attached) spatials
         * TODO include in LaunchControl.setEnabled() ?
         */
        LaunchControl launchControl = spatial.getControl(LaunchControl.class);
        Spatial missile = launchControl.getMissile();
        if (launchControl.isReadyToLaunch()) {
            Shafts shafts = scene.getWorld().getShafts();
            shafts.release(spatial, missile);
        } else if (missile != null) {
            logger.log(Level.INFO, "deleting unready missile {0}",
                    missile.getName());
            scene.deleteSpatial(missile);
        }
        launchControl.setEnabled(false);
        /*
         * Reset both animation channels and disable the OtoAnimationControl.
         */
        AnimControl animControl = spatial.getControl(AnimControl.class);
        AnimChannel channel0 = animControl.getChannel(0);
        channel0.setAnim("stand");
        AnimChannel channel1 = animControl.getChannel(1);
        channel1.setAnim("stand");
        OtoAnimationControl otoAnimationControl =
                spatial.getControl(OtoAnimationControl.class);
        otoAnimationControl.setEnabled(false);
        SkeletonControl skeletonControl =
                spatial.getControl(SkeletonControl.class);
        skeletonControl.setEnabled(false);

        animControl.setEnabled(false);
        SignalControl signalControl = spatial.getControl(SignalControl.class);
        signalControl.setEnabled(false);
        /*
         * The oto converts to a bubble after it's been impaired for some time.
         */
        DeleteControl deleteControl = spatial.getControl(DeleteControl.class);

        CollisionShape shape = otoControl.getCollisionShape();
        PhysicsSpace space = scene.getWorld().getPhysics().getSpace();
        BatsBubbleControl bubble =
                new BatsBubbleControl(shape, space, deleteControl);
        spatial.addControl(bubble);

        BatsDelayControl delayControl = new BatsDelayControl();
        spatial.addControl(delayControl);
        delayControl.addSlave(bubble);
        delayControl.setRemainingSeconds(impairTimeout);
        delayControl.setEnabled(true);
    }

    /**
     * Test whether the specified oto is impaired.
     *
     * @param spatial which oto (not null)
     * @return true if the oto is impaired; false if not an oto or not impaired
     */
    @Override
    public boolean isImpaired(Spatial spatial) {
        OtoControl otoControl = spatial.getControl(OtoControl.class);
        if (otoControl == null) {
            return false;
        }
        boolean result = otoControl.isImpaired();
        return result;
    }

    /**
     * Reset an oto's head to its default position.
     *
     * @param spatial which oto to adjust (not null)
     */
    @Override
    public void resetViewpoint(Spatial spatial) {
        assert spatial != null;

        super.resetViewpoint(spatial);
        updateHead(spatial);
    }

    /**
     * Tilt an oto's head forward or back by a specified amount.
     *
     * @param spatial which oto to adjust (not null)
     * @param angle in radians, positive to tilt forward, negative to tilt back
     */
    @Override
    public void tiltViewpointForward(Spatial spatial, float angle) {
        assert spatial != null;

        float tiltAngle = SpatialProperties.getTiltAngle(spatial);
        float maxTilt = 0.3f;
        float minTilt = -1f;
        tiltAngle = FastMath.clamp(tiltAngle + angle, minTilt, maxTilt);
        SpatialProperties.setTiltAngle(spatial, tiltAngle);

        updateHead(spatial);
    }

    /**
     * Turn an oto's head left or right by a specified amount.
     *
     * @param spatial which oto to adjust (not null)
     * @param angle in radians, positive to turn left, negative to turn right
     */
    @Override
    public void turnViewpointLeft(Spatial spatial, float angle) {
        assert spatial != null;

        float turnAngle = SpatialProperties.getTurnAngle(spatial);
        float maxTurn = 1f;
        float minTurn = -1f;
        turnAngle = FastMath.clamp(turnAngle + angle, minTurn, maxTurn);
        SpatialProperties.setTurnAngle(spatial, turnAngle);

        updateHead(spatial);
    }

    /**
     * Update an oto's collision shape.
     *
     * @param spatial which oto to update (not null)
     */
    @Override
    public void updateCollisionShape(Spatial spatial) {
        OtoControl otoControl = spatial.getControl(OtoControl.class);
        otoControl.updateCollisionShape();
    }
    // *************************************************************************
    // private methods

    /**
     * Create two channels in an oto's AnimControl, one for the right arm and
     * another for the rest of the body, and use them to add an
     * OtoAnimationControl to the oto.
     *
     * @param spatial which oto (not null)
     */
    private void addAnimationControl(Spatial spatial) {
        AnimControl animControl = spatial.getControl(AnimControl.class);

        AnimChannel restOfBody = animControl.createChannel();
        AnimChannel rightArm = animControl.createChannel();

        rightArm.addFromRootBone("uparm.right");
        rightArm.setAnim("stand");

        restOfBody.addFromRootBone("hip.left");
        restOfBody.addFromRootBone("hip.right");
        restOfBody.addFromRootBone("uparm.left");
        restOfBody.setAnim("stand");

        OtoAnimationControl animationControl =
                new OtoAnimationControl(rightArm, restOfBody);
        spatial.addControl(animationControl);

        animationControl.setEnabled(true);
    }

    /**
     * Create a collision shape for an oto without legs.
     *
     * @param spatial which oto (not null)
     * @return a new instance
     */
    private CompoundCollisionShape createLeglessShape(Spatial spatial) {
        float relativeSize = getRelativeSize(spatial);
        float scaleFactor = scene.getScaleFactor();
        CompoundCollisionShape compound = new CompoundCollisionShape();

        float armLength = 0.5f * relativeSize; // meters
        float armRadius = 0.22f * relativeSize; // meters
        CapsuleCollisionShape arm = new CapsuleCollisionShape(
                armRadius / scaleFactor, armLength / scaleFactor);

        float x = 0.3f * relativeSize;
        float y = 0.26f * relativeSize;
        float z = 0f;
        Vector3f offset = new Vector3f(x, y, z);
        offset.divideLocal(scaleFactor);
        compound.addChildShape(arm, offset);

        arm = new CapsuleCollisionShape(armRadius / scaleFactor,
                armLength / scaleFactor);
        offset = new Vector3f(-x, y, z);
        offset.divideLocal(scaleFactor);
        compound.addChildShape(arm, offset);

        return compound;
    }

    /**
     * Access the local orientation of an oto's head.
     *
     * @param spatial which oto to measure (not null)
     * @return a new instance
     */
    private Quaternion getHeadOrientation(Spatial spatial) {
        assert spatial != null;

        float tiltAngle = SpatialProperties.getTiltAngle(spatial);
        float turnAngle = SpatialProperties.getTurnAngle(spatial);
        Quaternion result = new Quaternion();
        result.fromAngles(tiltAngle, turnAngle, 0f);
        return result;
    }

    /**
     * Update an oto's head position.
     *
     * @param spatial which oto to update (not null)
     */
    private void updateHead(Spatial spatial) {
        assert spatial != null;

        float tiltAngle = SpatialProperties.getTiltAngle(spatial);
        float turnAngle = SpatialProperties.getTurnAngle(spatial);
        Quaternion rot = new Quaternion();
        rot.fromAngles(0f, turnAngle, tiltAngle);
        Bone head = MySkeleton.getBone(spatial, "head");
        head.setUserTransforms(Vector3f.ZERO, rot, Vector3f.UNIT_XYZ);
    }
}